package com.cm.rpa;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import com.cm.kits.ChromeKits;
import com.cm.kits.JacksonKits;
import com.cm.kits.RandomKits;
import com.cm.rpa.beans.Point;
import com.cm.rpa.beans.Rectangle;
import com.cm.rpa.config.RpaConfig;
import com.cm.rpa.enums.MoveDirection;
import com.cm.rpa.exceptions.TimeoutException;
import com.fasterxml.jackson.databind.JsonNode;
import net.lightbody.bmp.BrowserMobProxyServer;
import net.lightbody.bmp.client.ClientUtil;
import net.lightbody.bmp.core.har.Har;
import net.lightbody.bmp.proxy.CaptureType;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.devtools.DevTools;
import org.openqa.selenium.devtools.v109.network.Network;
import org.openqa.selenium.devtools.v109.network.model.Response;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;


/**
 * 自动化机器人对象，对 selenium 功能进行封装，方便后期使用
 *
 * @author LWang 2023.01.28
 * @since 1.0.0
 */
public class RpaCore {

    /** ------------------------- 对象成员管理 ------------------------- */

    /**
     * RPA 对象状态
     */
    private boolean create = false;
    /**
     * 机器人配置
     */
    private final RpaConfig config;

    /**
     * 屏幕缩放比例（用于处理定位操作时按比例扩展坐标点）
     */
    private final double scale;

    /**
     * 浏览器驱动，Robot 核心对象
     */
    private ChromeDriver driver;

    /**
     * 获取当前驱动
     *
     * @return 当前驱动
     */
    public ChromeDriver driver() {
        return this.driver;
    }

    /**
     * 网络代理对象
     */
    private BrowserMobProxyServer proxy;

    /**
     * 当前浏览器对象右下角位置
     */
    private Point browserOffset;

    private Thread heartThread = null;
    private Map<String, Date> heartThreadCmd = new HashMap<>();

    private final static String ENTER = "enter";


    /** ------------------------- 构造方法 ------------------------- */

    /**
     * 自动化机器人初始化方法
     *
     * @param config 配置参数
     */
    public RpaCore(RpaConfig config) {

        this.config = config;
        this.scale = config.getScale();
        if (config.getRpa().isAutoCreate()) {
            this.create();
        }
        heartThread = new Thread(() -> {
            while (true) {
                try {
                    if (MapUtil.isEmpty(this.heartThreadCmd)) {
                        TimeUnit.SECONDS.sleep(1L);
                        continue;
                    }
                    // 获取回车命令
                    Date date = this.heartThreadCmd.get(ENTER);
                    if (date != null && System.currentTimeMillis() > date.getTime()) {
                        //发送回车命令
                        WinApi.enter();
                        this.clearCmd();
                        System.out.println(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss") + " heartThread已执行WinApi.enter()");
                    }

                } catch (Exception e) {

                }
            }
        });
        heartThread.start();
    }

    /** ------------------------- 核心对象处理 ------------------------- */

    /**
     * 销毁网络代理
     */
    private void destroyProxy() {
        this.endHar();
        if (!this.proxy.isStopped()) {
            this.proxy.stop();
        }
    }

    /**
     * 创建网络代理
     */
    private void createProxy() {
        this.proxy = new BrowserMobProxyServer();
        this.proxy.setTrustAllServers(true);
        this.proxy.start(this.config.getRpa().getProxy().getPort());
        this.proxy.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT, CaptureType.RESPONSE_CONTENT);
        this.proxy.setHarCaptureTypes(CaptureType.RESPONSE_CONTENT);
    }

    /**
     * RPA 对象状态
     *
     * @return 对象状态
     */
    public boolean isCreate() {
        return this.create;
    }

    public void stop() {
        if (!Objects.isNull(this.proxy)) {
            this.destroyProxy();
        }
        if (!Objects.isNull(this.driver)) {
            this.driver.quit();
            this.driver = null;
        }
        this.create = false;
    }

    /**
     * 创建 Driver 对象
     */
    public void create() {
        if (config.getRpa().getProxy().isEnable()) {
            this.createProxy();
        }
        var options = new ChromeOptions();
        //自动启动浏览器
        if (this.config.isRemoteDebugging() == false) {
            // Logger.getLogger("org.openqa.selenium.devtools").setLevel(Level.OFF);
            options.addArguments("--log-level=3");
            options.addArguments("--start-maximized", "disable-infobars", "--test-type", "--ignore-certificate-errors");
            options.setExperimentalOption("excludeSwitches", new String[]{"enable-automation", "enable-logging"});
            options.setExperimentalOption("prefs", new HashMap<String, Boolean>(3) {
                {
                    this.put("download.prompt_for_download", false);
                    this.put("download.directory_upgrade", false);
                    this.put("safebrowsing.enabled", false);
                }
            });
            if (this.config.getRpa().isFullScreen()) {
                options.addArguments("--kiosk");
            }
            //如果 不设置 --user-data-dir="C:\Program Files (x86)\scoped_dir3012_39031583
            // 那么 浏览器 每次启动就随机建目录，导致磁盘最后没了
            options.addArguments("--user-data-dir=/selenium/ChromeProfile");
        }
        // 手动启动浏览器
        if (this.config.isRemoteDebugging() == true) {
            ChromeKits.utilBrowserStart(this.config.getRemoteDebuggingCmd());
            options.setExperimentalOption("debuggerAddress", this.config.getRemoteDebuggingIP() + ":" + this.config.getRemoteDebuggingPort());
        }
        // Mobo
        if (this.config.getRpa().getProxy().isEnable()) {
            var seleniumProxy = ClientUtil.createSeleniumProxy(proxy);
            options.setCapability(CapabilityType.PROXY, seleniumProxy);
        }
        options.setCapability("acceptInsecureCerts", true);


        var builder = new ChromeDriverService.Builder();
        builder.usingDriverExecutable(this.config.getSelenium().getExe());
        var service = builder.build();
        this.driver = new ChromeDriver(service, options);
        if (this.config.getRpa().getCookies().isClean()) {
            this.driver.manage().deleteAllCookies();
        }
        HashMap<String, Object> cdpCmd = new HashMap<String, Object>();
        cdpCmd.put("source", """
                (function() {
                        try {
                            window.alert = function() {}
                            window.confirm = function() {return true;}
                        } catch (e) {
                        }
                    })()
                """);
        Map<String, Object> tmp_res = driver.executeCdpCommand("Page.addScriptToEvaluateOnNewDocument", cdpCmd);
        this.create = true;
    }

    /**
     * 获取当前对象 URL
     *
     * @return URL
     */
    public String url() {
        return this.driver.getCurrentUrl();
    }
    /** ------------------------- 网络代理使用 ------------------------- */

    private final AtomicInteger harCount = new AtomicInteger();

    private static final int DELTA = 1;

    /**
     * 停止网络监听统计超时时间（单位：秒）
     */
    private static final long QUIT_PERIOD = 1;


    /**
     * 网络监听超时时间（单位：秒）
     */
    private static final long LISTEN_TIME_OUT = 30;

    private void endHar() {
        try {
            if (!Objects.isNull(this.proxy.getHar())) {
                this.proxy.endHar();
            }
        } catch (Exception ignored) {

        }
    }

    /**
     * 开启一个新的监听集合
     */
    public Har newHar() {
        //Mobo模式开启
        if (this.config.isNetworkProxy() == false) {
            if (!this.config.getRpa().getProxy().isEnable()) {
                throw new IllegalStateException("当前 Robot 对象没有开启网络代理！");
            }
            this.endHar();
            return this.proxy.newHar(String.format("robot_har_%d", this.harCount.getAndAdd(DELTA)));
        }
        //chrome模式开启
        else {
            this.enableNetWork();
            return null;
        }
    }

    /**
     * 结束当前网络监听集合
     */
    public void stopHar() {
        if (this.config.isNetworkProxy() == false) {
            this.proxy.waitForQuiescence(QUIT_PERIOD, QUIT_PERIOD, TimeUnit.SECONDS);
            return;
        } else {
            this.closeNetWork();
        }
    }

    /**
     * 监听指定 uri 返回内容
     *
     * @param uri 要监听结果的 uri
     * @return 监听响应
     */
    public String listen(String uri) {
        return this.listen(uri, LISTEN_TIME_OUT);
    }

    /**
     * 监听指定 uri 返回内容
     *
     * @param uri     要监听结果的 uri
     * @param timeout 监听超时时间，（单位：秒）
     * @return 监听响应
     */
    public String listen(String uri, long timeout) {
        //Mobo模式的network
        if (this.config.isNetworkProxy() == false) {
            final var outTime = System.currentTimeMillis() + timeout * 1000;
            try {
                while (true) {
                    var har = this.proxy.getHar();
                    for (var entry : har.getLog().getEntries()) {
                        if (!Objects.isNull(entry.getRequest()) && entry.getRequest().getUrl().contains(uri) && !Objects.isNull(entry.getResponse()) && entry.getResponse().getStatus() == 200) {
                            return entry.getResponse().getContent().getText();
                        }
                    }
                    if (System.currentTimeMillis() > outTime) {
                        throw new TimeoutException(String.format("监听 %s 的响应超时；", uri));
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException ignored) {
                    }
                }
            } finally {
                this.stopHar();
            }
        }
        //chrome的network
        else {
            return this.listenNetWorkCenter(uri, timeout * 1000L);
        }
    }

    /**
     * 监听指定 uri 返回内容，并将返回内容反序列化为 JsonNode 对象
     *
     * @param uri 要监听结果的 uri
     * @return 监听结果
     */
    public JsonNode jsonListen(String uri) {
        return this.jsonListen(uri, LISTEN_TIME_OUT);
    }

    /**
     * 监听指定 uri 内容，并将返回内容反序列化为指定对象实例
     *
     * @param uri   要监听结果的 uri
     * @param clazz 反序列化对象类
     * @param <T>   反序列化对象类泛型
     * @return 监听结果
     */
    public <T> T jsonListen(String uri, Class<T> clazz) {
        return this.jsonListen(uri, LISTEN_TIME_OUT, clazz);
    }

    /**
     * 监听指定 uri 返回内容，并将返回内容反序列化为 JsonNode 对象
     *
     * @param uri     要监听结果的 uri
     * @param timeout 监听超时时间，（单位：秒）
     * @return 监听结果
     */
    public JsonNode jsonListen(String uri, long timeout) {
        var content = this.listen(uri, timeout);
        return JacksonKits.toJsonNode(content);
    }

    /**
     * 监听指定 uri 内容，并将返回内容反序列化为指定对象实例
     *
     * @param uri     要监听结果的 uri
     * @param timeout 监听超时时间，（单位：秒）
     * @param clazz   反序列化对象类
     * @param <T>     反序列化对象类泛型
     * @return 监听结果
     */
    public <T> T jsonListen(String uri, long timeout, Class<T> clazz) {
        var content = this.listen(uri, timeout);
        return JacksonKits.toBean(content, clazz);
    }

    /** ------------------------- 页面访问管理 ------------------------- */

    /**
     * 访问指定页面
     *
     * @param url 页面 URL
     */
    public void visit(String url) {
        try {
            this.driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
            this.driver.get(url);
            // 计算浏览器右下角位置
            var x = this.driver.executeScript("return window.outerWidth - window.innerWidth");
            var y = this.driver.executeScript("return window.outerHeight - window.innerHeight");
            if (x instanceof Number pX && y instanceof Number pY) {
                browserOffset = new Point(pX.doubleValue() * this.scale, pY.doubleValue() * this.scale);
            }
        } catch (org.openqa.selenium.TimeoutException e) {
            this.executeJavaScript("window.stop()", Objects.class);
        }
    }

    /**
     * 切换 Frame
     *
     * @param element 要切换到的 Frame 元素
     */
    public void switchToFrame(WebElement element) {
        this.driver.switchTo().frame(element);
    }

    /**
     * 切换 Frame
     *
     * @param by       要切换的 Frame 查询方式
     * @param waitTime 元素查找等待时间
     */
    public void switchToFrame(By by, long waitTime) {
        this.switchToFrame(this.findElement(by, waitTime));
    }

    /**
     * 切换到根 Frame（顶层）
     */
    public void switchToParent() {
        this.driver.switchTo().parentFrame();
    }

    /**
     * 刷新浏览器
     */
    public void refresh() {
        this.driver.navigate().refresh();
    }


    /**
     * 从浏览器中移除指定的 cookies
     *
     * @param cookies 要移除的 cookies
     */
    public void removeCookies(String... cookies) {
        if (Objects.isNull(this.driver)) {
            return;
        }
        for (var cookie : cookies) {
            this.driver.manage().deleteCookieNamed(cookie);
        }
    }

    /** ------------------------- 页面元素查询 ------------------------- */

    /**
     * 默认元素等待时间 (60 秒)
     */
    private static final long DEFAULT_WAIT_TIME = 60;

    /**
     * 判断元素是否可以点击
     *
     * @param element 要判断的元素
     * @return 点击状态
     */
    private boolean canClick(WebElement element) {
        try {
            var wait = new WebDriverWait(this.driver, Duration.of(5, TimeUnit.SECONDS.toChronoUnit()));
            wait.until(ExpectedConditions.elementToBeClickable(element));
            return true;
        } catch (Exception ignored) {
        }
        return false;
    }

    /**
     * 等待元素加载并可见
     *
     * @param by       查询元素
     * @param waitTime 等待时间（单位：秒）
     */
    public void waitElement(By by, long waitTime) {
        try {
            var wait = new WebDriverWait(this.driver, Duration.of(waitTime, TimeUnit.SECONDS.toChronoUnit()));
            wait.until(ExpectedConditions.presenceOfElementLocated(by));
            wait.until(ExpectedConditions.visibilityOfElementLocated(by));
        } catch (Exception ex) {
            // 如果等待元素超时，改为抛出元素不存在异常
            throw new NoSuchElementException(by.toString());
        }
    }

    /**
     * 查找元素
     *
     * @param by 要查找的元素
     * @return 查找到的元素
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    public WebElement findElement(By by) {
        return findElement(by, DEFAULT_WAIT_TIME);
    }

    /**
     * 查找元素
     *
     * @param by       要查找的元素
     * @param waitTime 等待元素可用时间（单位：秒）
     * @return 查找到的元素
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    public WebElement findElement(By by, long waitTime) {
        this.waitElement(by, waitTime);
        return this.driver.findElement(by);
    }

    /**
     * 从已知元素中获取元素
     *
     * @param element 查询元素范围（祖先级元素）
     * @param by      要查询的元素
     * @return 查找到的元素
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    public WebElement findElement(WebElement element, By by) {
        return element.findElement(by);
    }


    /**
     * 查找元素集合
     *
     * @param by 要查找的元素集合
     * @return 查找到的元素集合
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    public List<WebElement> findElements(By by) {
        return findElements(by, DEFAULT_WAIT_TIME);
    }

    /**
     * 查找元素集合
     *
     * @param by       要查找的元素集合
     * @param waitTime 等待元素可用的时间（单位：秒）
     * @return 查找到的元素集合
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    public List<WebElement> findElements(By by, long waitTime) {
        this.waitElement(by, waitTime);
        return this.driver.findElements(by);
    }

    /**
     * 从已知元素中查询元素列表
     *
     * @param element 查询元素范围（祖先级元素）
     * @param by      要查询的元素
     * @return 查找到的元素集合
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    public List<WebElement> findElements(WebElement element, By by) {
        return element.findElements(by);
    }

    /**
     * 查找内部文本符合条件的元素
     *
     * @param innerText 内部文本
     * @param by        要查找的元素
     * @param waitTime  等待元素可用的时间（单位：秒）
     * @return 查找到的元素
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    public WebElement findElementWithInnerText(String innerText, By by, long waitTime) {
        return this.findElementWithInnerText(innerText, this.findElements(by, waitTime));
    }

    /**
     * 在指定范围内查找内部文本符合条件的元素
     *
     * @param innerText 内部文本
     * @param element   查询范围（祖先元素）
     * @param by        要查找的元素
     * @return 查找到的元素
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    public WebElement findElementWithInnerText(String innerText, WebElement element, By by) {
        return this.findElementWithInnerText(innerText, this.findElements(element, by));
    }


    /**
     * 在元素列表中查找内部文本符合条件的元素
     *
     * @param innerText 内部文本
     * @param elements  要查找的元素列表
     * @return 查找到的元素
     * @throws NoSuchElementException，元素不存在或可用超时抛出
     */
    private WebElement findElementWithInnerText(String innerText, List<WebElement> elements) {
        if (elements != null && !elements.isEmpty()) {
            for (var element : elements) {
                if (this.textEquals(element, innerText)) {
                    return element;
                }
            }
        }
        throw new NoSuchElementException("inner text: " + innerText);
    }

    /** ------------------------- 页面元素交互 ------------------------- **/

    /**
     * 计算元素矩形点
     *
     * @return 元素矩形点
     */
    public Rectangle sumElementRectangle(WebElement element) {
        var result = JacksonKits.toBean(this.executeJavaScript("""
                function get_offset_of_parent(win) {
                    if (win != win.parent) {
                        var parentOffset = get_offset_of_parent(win.parent);
                        var frames = parent.document.getElementsByTagName("iframe");
                        for (var i = 0; i < frames.length; i ++) {
                            if (win == frames[i].contentWindow) {
                                // 当前窗口，获取当前窗口偏移量
                                rect = frames[i].getBoundingClientRect()
                                return {
                                    x: parentOffset.x + rect.left,
                                    y: parentOffset.y + rect.top
                                };
                            }
                        }
                    } else {
                        return {
                            x: 0,
                            y: 0
                        };
                    }
                }
                return JSON.stringify(get_offset_of_parent(window));
                """, String.class), Point.class);

        assert result != null;
        var leftTopOffset = new Point(this.browserOffset.x() + result.x() * this.scale, this.browserOffset.y() + result.y() * this.scale);

        var elementRect = JacksonKits.toJsonNode(this.executeJavaScript("return JSON.stringify(arguments[0].getBoundingClientRect())", String.class, element));

        var lt = new Point((leftTopOffset.x() + this.readFromJsonNode(elementRect, "left")) * this.scale, (leftTopOffset.y() + this.readFromJsonNode(elementRect, "top")) * this.scale);
        var rt = new Point((leftTopOffset.x() + this.readFromJsonNode(elementRect, "right")) * this.scale, (leftTopOffset.y() + this.readFromJsonNode(elementRect, "top")) * this.scale);
        var rb = new Point((leftTopOffset.x() + this.readFromJsonNode(elementRect, "right")) * this.scale, (leftTopOffset.y() + this.readFromJsonNode(elementRect, "bottom")) * this.scale);
        var lb = new Point((leftTopOffset.x() + this.readFromJsonNode(elementRect, "left")) * this.scale, (leftTopOffset.y() + this.readFromJsonNode(elementRect, "bottom")) * this.scale);
        return new Rectangle(lt, rt, rb, lb);
    }

    /**
     * 从 JsonNode 中读取元素，并转换为 double
     *
     * @param node JsonNode 对象
     * @param key  JsonNode key
     * @return 值
     */
    private double readFromJsonNode(JsonNode node, String key) {
        return node.get(key).doubleValue();
    }

    /**
     * 执行 JavaScript 脚本，并返回结果
     *
     * @param script 要执行的脚本
     * @param clazz  脚本返回类型
     * @param args   脚本参数
     * @param <T>    返回类型泛型
     * @return 执行结果
     */
    public <T> T executeJavaScript(String script, Class<T> clazz, Object... args) {
        var result = this.driver.executeScript(script, args);
        if (!Objects.isNull(result) && clazz.isInstance(result)) {
            return (T) result;
        }
        return null;
    }

    public void click(By by) {
        this.click(by, DEFAULT_WAIT_TIME);
    }

    public void click(By by, long waitTime) {
        var element = this.findElement(by, waitTime);
        this.click(element);
    }

    public void click(WebElement element) {
        if (Objects.isNull(element)) {
            return;
        }
        this.scrollTo(element);
        if (!this.config.getRpa().isSimulate() && this.canClick(element)) {
            element.click();
        } else {
            this.moveTo(element);
            WinApi.leftClick();
        }
    }

    /**
     * 向指定元素输出文本
     *
     * @param by   元素
     * @param text 文本
     */
    public void sendKey(By by, String text) {
        this.sendKey(by, text, false, DEFAULT_WAIT_TIME);
    }

    /**
     * 向指定元素输出文本
     *
     * @param by       元素
     * @param text     文本
     * @param simulate 是否按单字符模拟输入
     */
    public void sendKey(By by, String text, boolean simulate) {
        this.sendKey(by, text, simulate, DEFAULT_WAIT_TIME);
    }

    /**
     * 向指定元素输出文本
     *
     * @param by       元素
     * @param text     文本
     * @param simulate 是否按单字符模拟输入
     * @param waitTime 元素查找超时时间（单位：秒）
     */
    public void sendKey(By by, String text, boolean simulate, long waitTime) {
        var element = this.findElement(by, waitTime);
        this.sendKey(element, text, simulate);
    }

    /**
     * 向指定元素输出文本
     *
     * @param element 元素
     * @param text    文本
     */
    public void sendKey(WebElement element, String text) {
        this.sendKey(element, text, false);
    }

    /**
     * 向指定元素输出文本
     *
     * @param element  元素
     * @param text     文本
     * @param simulate 是否按单字符模拟输入
     */
    public void sendKey(WebElement element, String text, boolean simulate) {
        if (Objects.isNull(element) || Objects.isNull(text) || "".equals(text)) {
            return;
        }
        this.scrollTo(element);
        var readonly = element.getAttribute("readonly");
        try {
            this.driver.executeScript("arguments[0].removeAttribute('readonly')", element);
            if (simulate) {
                var duration = RandomKits.nextInt(10, 50);
                for (var s : text.split("")) {
                    element.sendKeys(s);
                    WinApi.delay(duration);
                }
            } else {
                element.sendKeys(text);
            }
        } finally {
            this.driver.executeScript("arguments[0].setAttribute('readonly', arguments[1])", element, readonly);
        }
    }

    /**
     * 滚动到指定元素
     *
     * @param by 元素
     */
    public void scrollTo(By by) {
        this.scrollTo(by, DEFAULT_WAIT_TIME);
    }

    /**
     * 滚动到指定元素
     *
     * @param by       元素
     * @param waitTime 元素查找超时时间（单位：秒）
     */
    public void scrollTo(By by, long waitTime) {
        var element = this.findElement(by, waitTime);
        this.scrollTo(element);
    }

    /**
     * 滚动到指定元素
     *
     * @param element 元素
     */
    public void scrollTo(WebElement element) {
        // 通过 JavaScript 将元素滚动到可见范围
        this.driver.executeScript("arguments[0].scrollIntoView()", element);
    }

    /**
     * 将鼠标指针移动到指定元素中
     *
     * @param by 元素
     */
    public void moveTo(By by) {
        this.moveTo(by, DEFAULT_WAIT_TIME, MoveDirection.DIAGONAL);
    }

    /**
     * 将鼠标指针移动到指定元素中
     *
     * @param by        元素
     * @param direction 鼠标移动方向
     */
    public void moveTo(By by, MoveDirection direction) {
        this.moveTo(by, DEFAULT_WAIT_TIME, direction);
    }

    /**
     * 将鼠标指针移动到指定元素中
     *
     * @param by       元素
     * @param waitTime 元素查找超时时间（单位：秒）
     */
    public void moveTo(By by, long waitTime) {
        this.moveTo(by, waitTime, MoveDirection.DIAGONAL);
    }

    /**
     * 将鼠标指针移动到指定元素中
     *
     * @param by        元素
     * @param waitTime  元素查找超时时间（单位：秒）
     * @param direction 鼠标移动方向
     */
    public void moveTo(By by, long waitTime, MoveDirection direction) {
        var element = this.findElement(by, waitTime);
        this.moveTo(element, direction);
    }

    /**
     * 将鼠标指针移动到指定元素中
     *
     * @param element 元素
     */
    public void moveTo(WebElement element) {
        this.moveTo(element, MoveDirection.DIAGONAL);
    }

    /**
     * 将鼠标指针移动到指定元素中
     *
     * @param element   元素
     * @param direction 鼠标移动方向
     */
    public void moveTo(WebElement element, MoveDirection direction) {
        var rect = this.sumElementRectangle(element);
        var point = new Point((rect.leftTop().x() + rect.rightBottom().x()) / 2, (rect.leftTop().y() + rect.rightBottom().y()) / 2);
        this.moveTo(point, direction);
    }

    /**
     * 将鼠标移动到指定点
     *
     * @param point 点
     */
    public void moveTo(Point point) {
        this.moveTo(point, MoveDirection.DIAGONAL);
    }

    /**
     * 将鼠标移动到指定点
     *
     * @param point     点
     * @param direction 鼠标移动方向
     */
    public void moveTo(Point point, MoveDirection direction) {
        var currentPoint = WinApi.currentPoint();
        var duration = RandomKits.nextLong(100, 1000);
        switch (direction) {
            case HORIZONTAL -> WinApi.mouseMove(point.x(), currentPoint.y(), duration);
            case VERTICAL -> WinApi.mouseMove(currentPoint.x(), point.y(), duration);
        }
        WinApi.mouseMove(point.x(), point.y(), duration);
    }

    /**
     * 将鼠标拖拽到指定点
     *
     * @param point 目标点
     */
    public void dragTo(Point point) {
        var duration = RandomKits.nextLong(100, 1000);
        WinApi.dragTo(point.x(), point.y(), duration);
    }

    /**
     * 使用selenium方式拖拽元素一定的偏移距离
     *
     * @param dragHandler
     * @param xoffset
     */
    public void dragCenter(WebElement dragHandler, int xoffset) {
        this.resetInputState();
        Actions action = new Actions(this.driver);
        action.moveToElement(dragHandler).clickAndHold().build().perform();
        int currentOffset = 0;
        int setp = 10;
        while (true) {
            action = new Actions(this.driver);
            action.moveByOffset(setp, 0).build().perform();
            currentOffset += setp;
            if (currentOffset >= xoffset) {
                break;
            }
            this.sleep(1);
        }
        action = new Actions(this.driver);
        action.release().build().perform();
    }

    public void drag(WebElement dragHandler, WebElement dragHandlerContiner) {
//        Rectangle handlerRect = this.sumElementRectangle(dragHandlerContiner);
        int xoffset = dragHandlerContiner.getRect().width;
        this.dragCenter(dragHandler, xoffset);
    }

    /** ------------------------- 辅助方法 ------------------------- */

    /**
     * 空字符串
     */
    private static final String EMPTY_TEXT = "";

    /**
     * 去除字符串前后空字符，封装下，减少为空判断
     *
     * @param text 要处理的字符串
     * @return 处理后的字符串
     */
    private String trim(String text) {
        if (text == null) {
            return EMPTY_TEXT;
        }
        return text.trim();
    }

    /**
     * 判断元素内容是否包含指定文本
     *
     * @param element 要判断的元素
     * @param text    要包含的文本
     * @return 判断结果
     */
    public boolean inElementText(WebElement element, String text) {
        if (element == null) {
            return false;
        }
        if (StringUtils.isBlank(text)) {
            return true;
        }
        var innerText = this.trim(element.getText());
        return StringUtils.contains(innerText, text);
    }

    /**
     * 判断元素内容是否符合条件
     *
     * @param element   要判断的元素
     * @param innerText 要符合的文本
     * @return 比对结果
     */
    public boolean textEquals(WebElement element, String innerText) {
        if (element == null) {
            return false;
        }
        var text = this.trim(element.getText());
        return Objects.equals(text, innerText);
    }

    /**
     * 通过 Windows API 休眠线程（不用考虑异常）
     *
     * @param ms 休眠时间（单位：秒）
     */
    public void sleep(int ms) {
        WinApi.delay(ms);
    }

    public boolean isAlertPersent() {
        try {
            this.driver.switchTo().alert();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public String getAlert() {
        String currentWindowHandle = this.getCurrentWindowHandle();
        Alert alert = this.driver.switchTo().alert();
        String text = alert.getText();
        alert.accept();
        this.switchWindow(currentWindowHandle);
        return text;
    }

    public void acceptAlert() {
        String currentWindowHandle = this.getCurrentWindowHandle();
        Alert alert = this.driver.switchTo().alert();
        alert.accept();
        this.switchWindow(currentWindowHandle);
    }

    public void sendEnterCmd(int offsetMiliSeconds) {
        System.out.println(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss") + "driver 已发送 enter");
        Date exeDate = new Date(System.currentTimeMillis() + offsetMiliSeconds);
        //发送命令
        this.heartThreadCmd.put(ENTER, exeDate);
        //尝试点击alert
        if (this.isAlertPersent()) {
            this.acceptAlert();
        }
        System.out.println(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss") + "driver 已经清空 enter");
        this.clearCmd();
    }

    public void clearCmd() {
        this.heartThreadCmd.clear();
    }

    public Set<String> getWindowHandles() {
        try {
            return this.driver.getWindowHandles();
        } catch (Exception e) {
            return null;
        }
    }

    public String getCurrentWindowHandle() {
        try {
            return this.driver.getWindowHandle();
        } catch (Exception e) {
            return "";
        }
    }

    public void switchWindow(String nameOrHandle) {
        try {
            this.driver.switchTo().window(nameOrHandle);
        } catch (Exception e) {
            throw e;
        }
    }

    private ConcurrentHashMap<String, String> networkMap = new ConcurrentHashMap<>();

    public void enableNetWork() {
        //开启监听
        DevTools devTools = this.driver.getDevTools();
        devTools.createSession();
        devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));
        //增加全部的请求
        devTools.addListener(Network.responseReceived(), res -> {
            try {
                networkMap.put(res.getResponse().getUrl(), devTools.send(Network.getResponseBody(res.getRequestId())).getBody());
            } catch (Exception ex) {

            }
        });

    }

    public String listenNetWorkCenter(String matchUrl, Long timeoutMs) {
        try {
            Date startTime = new Date();
            while (true) {
                for (Map.Entry<String, String> item : networkMap.entrySet()) {
                    if (StringUtils.contains(item.getKey(), matchUrl)) {
                        return item.getValue();
                    }
                }
                this.sleep(10);
                if (System.currentTimeMillis() - startTime.getTime() > timeoutMs) {
                    throw new TimeoutException("listenNetWork time out");
                }
            }
        } finally {
            this.closeNetWork();
        }
    }

    private String listenNetWorkCenterExample(String matchUrl, Long timeoutMs) {

        DevTools devTools = this.driver.getDevTools();
        try {
            AtomicReference<String> responseBody = new AtomicReference<>("");
            // 获取Response响应内容
            devTools.addListener(Network.responseReceived(), res -> {
                Response response = res.getResponse();
                System.out.println("response.getUrl():" + response.getUrl());
                if (response.getUrl().contains(matchUrl)) {
                    if (StringUtils.isBlank(responseBody.get())) {
                        // 获取network 请求的responseBody
                        responseBody.set(devTools.send(Network.getResponseBody(res.getRequestId())).getBody());
                    }
                }
            });
            Date startTime = new Date();
            while (true) {
                this.sleep(10);
                if (StringUtils.isNotBlank(responseBody.get())) {
                    return responseBody.get();
                }
                if (System.currentTimeMillis() - startTime.getTime() > timeoutMs) {
                    throw new TimeoutException("listenNetWork time out");
                }
            }
        } finally {
            this.closeNetWork();
        }
    }

    public void closeNetWork() {
        try {
            this.driver.getDevTools().send(Network.disable());
        } catch (Exception e) {

        } finally {
            this.networkMap.clear();
        }
    }

    public void resetInputState() {
        this.driver.resetInputState();
    }
}

